package com.github.aaronshan.functions.regexp.re2j;

import io.airlift.slice.Slice;

import java.io.Serializable;

import static com.github.aaronshan.functions.regexp.re2j.Options.DEFAULT_OPTIONS;
import static com.github.aaronshan.functions.regexp.re2j.RE2.Anchor.ANCHOR_BOTH;
import static com.github.aaronshan.functions.regexp.re2j.RE2.Anchor.UNANCHORED;
import static com.github.aaronshan.functions.regexp.re2j.RE2.MatchKind.FIRST_MATCH;

/**
 * A compiled representation of an RE2 regular expression, mimicking the
 * {@code java.util.regex.Pattern} API.
 *
 * <p>The matching functions take {@code String} arguments instead of
 * the more general Java {@code CharSequence} since the latter doesn't
 * provide UTF-16 decoding.
 *
 * <p>See the <a href='package-summary.html'>package-level
 * documentation</a> for an overview of how to use this API.</p>
 *
 * @author rsc@google.com (Russ Cox)
 */
public final class Pattern implements Serializable {
    /**
     * Flag: case insensitive matching.
     */
    public static final int CASE_INSENSITIVE = 1;

    /**
     * Flag: dot ({@code .}) matches all characters, including newline.
     */
    public static final int DOTALL = 2;

    /**
     * Flag: multiline matching: {@code ^} and {@code $} match at
     * beginning and end of line, not just beginning and end of input.
     */
    public static final int MULTILINE = 4;

    /**
     * Flag: Unicode groups (e.g. {@code \p\{Greek\}}) will be syntax errors.
     */
    public static final int DISABLE_UNICODE_GROUPS = 8;
    private static final long serialVersionUID = 0;
    // The pattern string at construction time.
    private final String pattern;
    // The flags at construction time.
    private final int flags;
    // The options at construction time.
    private final Options options;
    // The compiled RE2 regexp.
    private transient final RE2 re2;

    // This is visible for testing.
    Pattern(String pattern, int flags, RE2 re2, Options options) {
        if (pattern == null) {
            throw new NullPointerException("pattern is null");
        }
        if (re2 == null) {
            throw new NullPointerException("re2 is null");
        }
        this.pattern = pattern;
        this.flags = flags;
        this.options = options;
        this.re2 = re2;
    }

    /**
     * Creates and returns a new {@code Pattern} corresponding to
     * compiling {@code regex} with the default flags (0).
     *
     * @param regex the regular expression
     * @return returns a new {@code Pattern} corresponding to compiling {@code regex} with the default flags (0).
     * @throws PatternSyntaxException if the pattern is malformed
     */
    public static Pattern compile(String regex) {
        return compile(regex, DEFAULT_OPTIONS);
    }

    public static Pattern compile(String regex, Options options) {
        return compile(regex, regex, 0, options);
    }

    /**
     * Creates and returns a new {@code Pattern} corresponding to
     * compiling {@code regex} with the default flags (0).
     *
     * @param regex the regular expression
     * @param flags bitwise OR of the flag constants {@code CASE_INSENSITIVE},
     *              {@code DOTALL}, and {@code MULTILINE}
     * @return a new {@code Pattern} corresponding to compiling {@code regex} with the default flags (0).
     * @throws PatternSyntaxException   if the regular expression is malformed
     * @throws IllegalArgumentException if an unknown flag is given
     */
    public static Pattern compile(String regex, int flags) {
        return compile(regex, flags, DEFAULT_OPTIONS);
    }

    public static Pattern compile(String regex, int flags, Options options) {
        String flregex = regex;
        if ((flags & CASE_INSENSITIVE) != 0) {
            flregex = "(?i)" + flregex;
        }
        if ((flags & DOTALL) != 0) {
            flregex = "(?s)" + flregex;
        }
        if ((flags & MULTILINE) != 0) {
            flregex = "(?m)" + flregex;
        }
        if ((flags & ~(MULTILINE | DOTALL | CASE_INSENSITIVE | DISABLE_UNICODE_GROUPS)) != 0) {
            throw new IllegalArgumentException("Flags should only be a combination " +
                    "of MULTILINE, DOTALL, CASE_INSENSITIVE, DISABLE_UNICODE_GROUPS");
        }
        return compile(flregex, regex, flags, options);
    }

    /**
     * Helper: create new Pattern with given regex and flags.
     * Flregex is the regex with flags applied.
     */
    private static Pattern compile(String flregex, String regex, int flags, Options options) {
        int re2Flags = RE2.PERL;
        if ((flags & DISABLE_UNICODE_GROUPS) != 0) {
            re2Flags &= ~RE2.UNICODE_GROUPS;
        }
        return new Pattern(regex, flags, RE2.compileImpl(flregex, re2Flags, FIRST_MATCH, options), options);
    }

    /**
     * Matches a {@link Slice} against a regular expression.
     *
     * @param regex the regular expression
     * @param input the input
     * @return true if the regular expression matches the entire input
     * @throws PatternSyntaxException if the regular expression is malformed
     */
    public static boolean matches(String regex, Slice input) {
        return matches(regex, input, DEFAULT_OPTIONS);
    }

    public static boolean matches(String regex, Slice input, Options options) {
        return compile(regex, options).matches(input);
    }

    /**
     * Matches a {@link Slice} against a regular expression (unanchored).
     *
     * @param regex the regular expression
     * @param input the input
     * @return true if the regular expression matches the entire input
     * @throws PatternSyntaxException if the regular expression is malformed
     */
    public static boolean find(String regex, Slice input) {
        return find(regex, input, DEFAULT_OPTIONS);
    }

    public static boolean find(String regex, Slice input, Options options) {
        return compile(regex, options).find(input);
    }

    /**
     * Returns a literal pattern string for the specified
     * string.
     *
     * <p>This method produces a string that can be used to
     * create a <code>Pattern</code> that would match the string
     * <code>s</code> as if it were a literal pattern.</p> Metacharacters
     * or escape sequences in the input sequence will be given no special
     * meaning.
     *
     * @param s The string to be literalized
     * @return A literal string replacement
     */
    public static String quote(String s) {
        return RE2.quoteMeta(s);
    }

    /**
     * @return the flags used in the constructor.
     */
    public int flags() {
        return flags;
    }

    /**
     * @return the options used in the constructor.
     */
    public Options options() {
        return options;
    }

    /**
     * @return the pattern used in the constructor.
     */
    public String pattern() {
        return pattern;
    }

    RE2 re2() {
        return re2;
    }

    public boolean matches(Slice input) {
        return re2.match(input, 0, ANCHOR_BOTH, null, 0);
    }

    public boolean find(Slice input) {
        return re2.match(input, 0, UNANCHORED, null, 0);
    }

    /**
     * Creates a new {@code Matcher} matching the pattern against the input.
     *
     * @param input the input {@link Slice}
     * @return a new {@code Matcher} matching the pattern against the input.
     */
    public Matcher matcher(Slice input) {
        return new Matcher(this, input);
    }

    /**
     * Splits input around instances of the regular expression.
     * It returns an array giving the {@link Slice}s that occur before, between, and after instances
     * of the regular expression.  Empty {@link Slice}s that would occur at the end
     * of the array are omitted.
     *
     * @param input the input {@link Slice} to be split
     * @return the split {@link Slice}s
     */
    public Slice[] split(Slice input) {
        return split(input, 0);
    }

    /**
     * Splits input around instances of the regular expression.
     * It returns an array giving the {@link Slice}s that occur before, between, and after instances
     * of the regular expression.
     *
     * <p>If {@code limit <= 0}, there is no limit on the size of the returned array.
     * If {@code limit == 0}, empty {@link Slice}s that would occur at the end of the array are omitted.
     * If {@code limit > 0}, at most limit {@link Slice}s are returned.  The final {@link Slice} contains
     * the remainder of the input, possibly including additional matches of the pattern.
     *
     * @param input the input {@link Slice} to be split
     * @param limit the limit
     * @return the split {@link Slice}s
     */
    public Slice[] split(Slice input, int limit) {
        return split(new Matcher(this, input), limit);
    }

    /**
     * Helper: run split on m's input.
     */
    private Slice[] split(Matcher m, int limit) {
        int matchCount = 0;
        int arraySize = 0;
        int last = 0;
        while (m.find()) {
            matchCount++;
            if (limit != 0 || last < m.start()) {
                arraySize = matchCount;
            }
            last = m.end();
        }
        if (last < m.inputLength() || limit != 0) {
            matchCount++;
            arraySize = matchCount;
        }

        int trunc = 0;
        if (limit > 0 && arraySize > limit) {
            arraySize = limit;
            trunc = 1;
        }
        Slice[] array = new Slice[arraySize];
        int i = 0;
        last = 0;
        m.reset();
        while (m.find() && i < arraySize - trunc) {
            array[i++] = m.substring(last, m.start());
            last = m.end();
        }
        if (i < arraySize) {
            array[i] = m.substring(last, m.inputLength());
        }
        return array;
    }

    @Override
    public String toString() {
        return pattern;
    }

    /**
     * Returns the number of capturing groups in this matcher's pattern.
     * Group zero denotes the enture pattern and is excluded from this count.
     *
     * @return the number of capturing groups in this pattern
     */
    public int groupCount() {
        return re2.numberOfCapturingGroups();
    }

    Object readResolve() {
        // The deserialized version will be missing the RE2 instance, so we need to create a new,
        // compiled version.
        return Pattern.compile(pattern, flags, options);
    }
}

